# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
#     https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
#     https://docs.fastlane.tools/plugins/available-plugins
#

min_fastlane_version('2.156.0')
fastlane_require "dotenv"
fastlane_require "uri"

default_platform(:ios)
platform :ios do

  #### Pre ####

  before_all do
    # Ensure used Xcode version
    xcversion(version: "16.4")
  end

  #### Public ####

  desc "Builds an adhoc ipa"
  lane :adhoc do |options|
    options[:adhoc] = true
    if !options.has_key?(:build_number)
      build_number = generate_build_number()
      options = { build_number: build_number }.merge(options)
    end
    build_release(options)
  end

  desc "Builds an ipa for the App Store"
  lane :app_store do |options|
    UI.user_error!("'SENTRY_AUTH_TOKEN' environment variable should be set to use this lane, see Passbolt") unless !ENV["SENTRY_AUTH_TOKEN"].to_s.empty?
    sentry_check_cli_installed

    if !options.has_key?(:build_number)
      build_number = generate_build_number()
      options = { build_number: build_number }.merge(options)
    end
    build_release(options)
    upload_dsyms_to_sentry
  end

  desc "Builds an Alpha ipa for pull request branches"
  lane :alpha do
    # Check we have all the tokens and tools necessary
    UI.user_error!("'DIAWI_API_TOKEN' environment variable should be set to use this lane") unless !ENV["DIAWI_API_TOKEN"].to_s.empty?
            
    # Generate the "Alpha" app variant    
    setup_app_variant(name: "Alpha")    
    # Builds an Ad Hoc IPA
    adhoc
    # Upload to Diawi
    upload_to_diawi
  end

  desc "Upload IPA to Diawi"
  lane :upload_to_diawi do
    UI.user_error!("'DIAWI_API_TOKEN' environment variable should be set to use this lane") unless !ENV["DIAWI_API_TOKEN"].to_s.empty?

    # Upload to Diawi
    diawi(
      token: ENV["DIAWI_API_TOKEN"],
      wall_of_apps: false,
      file: lane_context[SharedValues::IPA_OUTPUT_PATH]
    )

    # Get the Diawi link from Diawi action shared value
    diawi_link = lane_context[SharedValues::UPLOADED_FILE_LINK_TO_DIAWI]
    UI.command_output("Diawi link: " + diawi_link.to_s)

    # Generate the Diawi QR code file link
    diawi_app_id = URI(diawi_link).path.split('/').last
    diawi_qr_code_link = "https://www.diawi.com/qrcode/link/#{diawi_app_id}"
    UI.command_output("Diawi QR code link: " + diawi_qr_code_link.to_s)

    # Set "DIAWI_FILE_LINK" to GitHub environment variables for Github actions
    if is_ci 
      sh("echo DIAWI_FILE_LINK=#{diawi_link} >> $GITHUB_ENV")
      sh("echo DIAWI_QR_CODE_LINK=#{diawi_qr_code_link} >> $GITHUB_ENV")
    end
  end

  desc "Builds the ipa for the AppStore, then uploads it"
  lane :deploy_release do |options|
    # build the IPA
    app_store(options)
    # upload the IPA and metadata
    deliver()

    # We haven't yet implemented/tested deliver/upload_to_appstore properly so keep it manual for now
    # UI.message("IPA is available at path '#{ENV['IPA_OUTPUT_PATH']}'. Please upload manually using Application Loader.")
    # UI.confirm("Have you uploaded the IPA to the AppStore now?")
    #
  end


  desc "Use an app variant. An app variant overwrite default project configuration or ressource files with custom values"
  lane :setup_app_variant do |options|
    appVariantScript = "../Variants/setup_app_variant.sh"    
    # Make sure app variant script can be executed
    sh "chmod 775 #{appVariantScript}"
    # Run app variant script with variant name
    sh(appVariantScript, options[:name]) 
    # Force reloading env variables
    Dotenv.overload(".env.default")
  end

  desc "Build the app for simulator to ensure it compiles"
  lane :build do |options|
    xcodegen(spec: "project.yml")
    cocoapods

    app_name = "Riot"
    build_app(
      clean: true,
      scheme: app_name,
      derived_data_path: "./DerivedData/",
      buildlog_path: "./DerivedData/Logs/",
      # skip_package_ipa: true,
      skip_archive: true,
      destination: "generic/platform=iOS Simulator",
    )
  end

  desc "Run tests"
  lane :test do
    cocoapods

    run_tests(
      workspace: "Riot.xcworkspace",
      scheme: "RiotSwiftUI",
      code_coverage: true,
      # Test result configuration
      result_bundle: true,
      output_directory: "./build/test",
      output_files: "riot.swiftui.unit.test.html,riot.swiftui.unit.test.report.junit.xml",
      open_report: !is_ci?
    )

    run_tests(
      workspace: "Riot.xcworkspace",
      scheme: "Riot",
      code_coverage: true,
      # Test result configuration
      result_bundle: true,
      output_directory: "./build/test",
      output_files: "riot.unit.test.html,riot.unit.test.report.junit.xml",
      open_report: !is_ci?
    )

    slather(
      cobertura_xml: true,
      output_directory: "./build/test",
      workspace: "Riot.xcworkspace",
      proj: "Riot.xcodeproj",
      scheme: "Riot",
      binary_basename: "Element",
    )
  end
  
  desc "Run UI tests"
  lane :uitest do
    cocoapods
    
    run_tests(
      workspace: "Riot.xcworkspace",
      scheme: "RiotSwiftUITests",
      device: "iPhone 15",
      code_coverage: true,
      # Test result configuration
      result_bundle: true,
      output_directory: "./build/test",
      output_files: "riot.swiftui.ui.test.html,riot.swiftui.ui.test.report.junit.xml",
      open_report: !is_ci?
    )

    slather(
      cobertura_xml: true,
      output_directory: "./build/test",
      workspace: "Riot.xcworkspace",
      proj: "Riot.xcodeproj",
      scheme: "RiotSwiftUITests",
      binary_basename: "RiotSwiftUI",
    )
  end


  #### Private ####

  desc "Download App Store or Ad-Hoc provisioning profiles"
  private_lane :build_release do |options|
    if is_ci
      UI.user_error!("'APPSTORECONNECT_KEY_ID' environment variable should be set to use this lane") unless !ENV["APPSTORECONNECT_KEY_ID"].to_s.empty?
      UI.user_error!("'APPSTORECONNECT_KEY_ISSUER_ID' environment variable should be set to use this lane") unless !ENV["APPSTORECONNECT_KEY_ISSUER_ID"].to_s.empty?
      UI.user_error!("'APPSTORECONNECT_KEY_CONTENT' environment variable should be set to use this lane") unless !ENV["APPSTORECONNECT_KEY_CONTENT"].to_s.empty?
      
      app_store_connect_api_key(
        key_id: ENV["APPSTORECONNECT_KEY_ID"],
        issuer_id: ENV["APPSTORECONNECT_KEY_ISSUER_ID"],
        key_content: ENV["APPSTORECONNECT_KEY_CONTENT"]
      )
    else
      UI.user_error!("'APPLE_ID' environment variable should be set to use this lane") unless !ENV["APPLE_ID"].to_s.empty?
    end

    build_number = options[:build_number]
    UI.user_error!("'build_number' parameter is missing") unless !build_number.to_s.empty?

    # ad-hoc or app-store?
    adhoc = options.fetch(:adhoc, false)

    # Extract git information to show within the app
    git_branch_name = options[:git_tag]
    if git_branch_name.to_s.empty?
      # Retrieve the current git branch as a fallback
      git_branch_name = mx_git_branch
    end
    UI.user_error!("Unable to retrieve GIT tag or branch") unless !git_branch_name.to_s.empty?

    # Fetch team id from Appfile
    team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id)
    UI.user_error!("Fail to fetch team id from Appfile") unless !team_id.to_s.empty?

    # Generate versioning preprocessor macros
    additional_preprocessor_definitions_hash = release_versioning_preprocessor_definitions(git_branch: git_branch_name, build_number: build_number)
    additional_preprocessor_definitions = additional_preprocessor_definitions_hash.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")

    # Generate xcodebuild additional arguments
    xcargs_hash = {
      "GCC_PREPROCESSOR_DEFINITIONS" => "$(GCC_PREPROCESSOR_DEFINITIONS) #{additional_preprocessor_definitions}",
      # Fix XCode 14 code signing issues for Swift packages containing resources bundles.
      "CODE_SIGN_STYLE" => "Manual",
    }

    xcargs = xcargs_hash.map { |k, v| "#{k}=#{v.shellescape}" }.join(" ")

    # Generate the project
    xcodegen(spec: "project.yml")
    
    # Clear derived data
    clear_derived_data(derived_data_path: ENV["DERIVED_DATA_PATH"])
    
    # Setup project provisioning profiles
    download_provisioning_profiles(adhoc: adhoc)
    disable_automatic_code_signing
    update_project_provisioning_profiles

    # Update build number
    update_build_number(build_number: build_number)

    # Perform a pod install
    cocoapods(repo_update: true)

    # Select a config
    if adhoc
      export_method = "ad-hoc"
      main_provisioning_profile = ENV["ADHOC_MAIN_PROVISIONING_PROFILE_SPECIFIER"]
      share_extension_provisioning_profile = ENV["ADHOC_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
      siri_intents_provisioning_profile = ENV["ADHOC_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
      nse_provisioning_profile = ENV["ADHOC_NSE_PROVISIONING_PROFILE_SPECIFIER"]
      broadcast_upload_extension_provisioning_profile = ENV["ADHOC_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
    else
      export_method = "app-store"
      main_provisioning_profile = ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"]
      share_extension_provisioning_profile = ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
      siri_intents_provisioning_profile = ENV["APPSTORE_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
      nse_provisioning_profile = ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"]
      broadcast_upload_extension_provisioning_profile = ENV["APPSTORE_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
    end

    # Build app and create ipa
    build_app(
      clean: true,
      scheme: ENV["SCHEME"],
      xcargs: xcargs,
      export_method: export_method,
      derived_data_path: ENV["DERIVED_DATA_PATH"],
      archive_path: ENV["ARCHIVE_PATH"],
      output_directory: ENV["BUILD_OUTPUT_DIRECTORY"],
      output_name: "#{ENV["IPA_NAME"]}.ipa",
      buildlog_path: ENV["BUILD_LOG_DIRECTORY"],
      codesigning_identity: ENV["APPSTORE_CODESIGNING_IDENTITY"],
      skip_profile_detection: true,
      export_options: {
        method: export_method,
        signingStyle: "manual",
        teamID: team_id,
        signingCertificate: ENV["APPSTORE_SIGNING_CERTIFICATE"],
        provisioningProfiles: {
          ENV["MAIN_BUNDLE_ID"] => main_provisioning_profile,
          ENV["SHARE_EXTENSION_BUNDLE_ID"] => share_extension_provisioning_profile,
          ENV["NSE_BUNDLE_ID"] => nse_provisioning_profile,
          ENV["SIRI_INTENTS_EXTENSION_BUNDLE_ID"] => siri_intents_provisioning_profile,
          ENV["BROADCAST_UPLOAD_EXTENSION_BUNDLE_ID"] => broadcast_upload_extension_provisioning_profile,
        },
        iCloudContainerEnvironment: "Production",
      },
    )
  end

  #### Private ####

  desc "Download App Store or Ad-Hoc provisioning profiles"
  private_lane :download_provisioning_profiles do |options|
    adhoc = options.fetch(:adhoc, false)

    output_path = ENV["PROVISIONING_PROFILES_PATH"]
    skip_certificate_verification = false

    main_provisioning_name = adhoc ? ENV["ADHOC_MAIN_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"]
    share_extension_provisioning_name = adhoc ? ENV["ADHOC_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
    siri_intents_provisioning_name = adhoc ? ENV["ADHOC_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
    notification_service_extension_provisioning_name = adhoc ? ENV["ADHOC_NSE_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"]
    broadcast_upload_extension_provisioning_name = adhoc ? ENV["ADHOC_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]

    # Main application
    get_provisioning_profile(
      app_identifier: ENV["MAIN_BUNDLE_ID"],
      provisioning_name: main_provisioning_name,
      ignore_profiles_with_different_name: true,
      adhoc: adhoc,
      skip_certificate_verification: skip_certificate_verification,
      output_path: output_path,
      filename: ENV["MAIN_PROVISIONING_PROFILE_FILENAME"],
      readonly: true,
    )
    # Share extension
    get_provisioning_profile(
      app_identifier: ENV["SHARE_EXTENSION_BUNDLE_ID"],
      provisioning_name: share_extension_provisioning_name,
      ignore_profiles_with_different_name: true,
      adhoc: adhoc,
      skip_certificate_verification: skip_certificate_verification,
      output_path: output_path,
      filename: ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
      readonly: true,
    )
    # Siri Intents extension
    get_provisioning_profile(
      app_identifier: ENV["SIRI_INTENTS_EXTENSION_BUNDLE_ID"],
      provisioning_name: siri_intents_provisioning_name,
      ignore_profiles_with_different_name: true,
      adhoc: adhoc,
      skip_certificate_verification: skip_certificate_verification,
      output_path: output_path,
      filename: ENV["SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
      readonly: true,
    )
    # NSE
    get_provisioning_profile(
      app_identifier: ENV["NSE_BUNDLE_ID"],
      provisioning_name: notification_service_extension_provisioning_name,
      ignore_profiles_with_different_name: true,
      adhoc: adhoc,
      skip_certificate_verification: skip_certificate_verification,
      output_path: output_path,
      filename: ENV["NSE_PROVISIONING_PROFILE_FILENAME"],
      readonly: true,
    )
    # Broadcast Upload Extension
    get_provisioning_profile(
      app_identifier: ENV["BROADCAST_UPLOAD_EXTENSION_BUNDLE_ID"],
      provisioning_name: broadcast_upload_extension_provisioning_name,
      ignore_profiles_with_different_name: true,
      adhoc: adhoc,
      skip_certificate_verification: skip_certificate_verification,
      output_path: output_path,
      filename: ENV["BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
      readonly: true,
    )
  end

  desc "Update provisioning profiles for each target"
  private_lane :update_project_provisioning_profiles do
    provisioning_profiles_path = ENV["PROVISIONING_PROFILES_PATH"]
    build_configuration = "Release"
    xcodeproj = ENV["PROJECT_PATH"]

    # Main application
    update_project_provisioning(
      xcodeproj: xcodeproj,
      profile: "#{provisioning_profiles_path}#{ENV["MAIN_PROVISIONING_PROFILE_FILENAME"]}",
      target_filter: ENV["MAIN_TARGET"],
      build_configuration: build_configuration,
    )
    # Share extension
    update_project_provisioning(
      xcodeproj: xcodeproj,
      profile: "#{provisioning_profiles_path}#{ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"]}",
      target_filter: ENV["SHARE_EXTENSION_TARGET"],
      build_configuration: build_configuration,
    )
    # Siri Intents extension
    update_project_provisioning(
      xcodeproj: xcodeproj,
      profile: "#{provisioning_profiles_path}#{ENV["SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_FILENAME"]}",
      target_filter: ENV["SIRI_INTENTS_EXTENSION_TARGET"],
      build_configuration: build_configuration,
    )
    # NSE
    update_project_provisioning(
      xcodeproj: xcodeproj,
      profile: "#{provisioning_profiles_path}#{ENV["NSE_PROVISIONING_PROFILE_FILENAME"]}",
      target_filter: ENV["NSE_TARGET"],
      build_configuration: build_configuration,
    )
    # Broadcast Upload Extension
    update_project_provisioning(
      xcodeproj: xcodeproj,
      profile: "#{provisioning_profiles_path}#{ENV["BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_FILENAME"]}",
      target_filter: ENV["BROADCAST_UPLOAD_EXTENSION_TARGET"],
      build_configuration: build_configuration,
    )
  end

  desc "Update application build number for all targets"
  private_lane :update_build_number do |options|
    build_number = options[:build_number]

    update_file_content(
      "../Config/AppVersion.xcconfig",
      /(CURRENT_PROJECT_VERSION\s*=)\s*.*/ => "\\1 #{build_number}"
    )
  end

  desc "Returns version identifiers hash to inject in GCC_PREPROCESSOR_DEFINITIONS for release builds"
  private_lane :release_versioning_preprocessor_definitions do |options|
    preprocessor_definitions = Hash.new

    git_branch_name = options[:git_branch]
    build_number = options[:build_number]

    if !git_branch_name.to_s.empty?
      preprocessor_definitions["GIT_BRANCH"] = git_branch_name.sub("origin/", "").sub("heads/", "")
    end

    if !build_number.to_s.empty?
      preprocessor_definitions["BUILD_NUMBER"] = build_number
    end

    preprocessor_definitions
  end

  desc "Upload dsym files to Sentry to symbolicate crashes"
  private_lane :upload_dsyms_to_sentry do
    UI.user_error!("'SENTRY_AUTH_TOKEN' environment variable should be set to use this lane") unless !ENV["SENTRY_AUTH_TOKEN"].to_s.empty?

    dsym_path = "#{ENV["BUILD_OUTPUT_DIRECTORY"]}/#{ENV["IPA_NAME"]}.app.dSYM.zip"
  
    sentry_upload_dif(
      auth_token: ENV["SENTRY_AUTH_TOKEN"],
      org_slug: 'element',
      project_slug: 'element-ios',
      url: 'https://sentry.tools.element.io/',
      path: dsym_path,
    )
  end

  # git_branch can return an empty screen with some CI tools (like GH actions) 
  # The CI build script needs to define MX_GIT_BRANCH with the right branch.
  def mx_git_branch
    mx_git_branch = git_branch
    if mx_git_branch == ""
        ensure_env_vars(
            env_vars: ['MX_GIT_BRANCH']
          )
        mx_git_branch = ENV["MX_GIT_BRANCH"]
    end
    return mx_git_branch
  end

  # Find the latest branch with the given name pattern in the given repo
  def find_branch(repo_slug, pattern)
    list = `git ls-remote --heads --sort=version:refname https://github.com/#{repo_slug} #{pattern}`
    list.split("\n")
        .map { |line| line.sub(%r{[0-9a-f]+\trefs/heads/}, '').chomp }
        .last # Latest ref found, in "version:refname" semantic order
  end
end

# Update an arbitrary file by applying some RegExp replacements to its content
#
# @param [String] file The path to the file that needs replacing
# @param [Hash<RegExp, String>] replacements A list of replacements to apply
#
def update_file_content(file, replacements)
  content = File.read(file)
  replacements.each do |pattern, replacement|
    content.gsub!(pattern, replacement)
  end
  File.write(file, content)
end

# Generates a new build number based on timestamp
def generate_build_number()
  require 'date'
  return DateTime.now.strftime("%Y%m%d%H%M%S")
end
